Skip to content

ZenNotes 2.30.0: Atlas, your vault as a map - #624

Merged
adibhanna merged 15 commits into
mainfrom
v2.30.0
Aug 19, 2026
Merged

ZenNotes 2.30.0: Atlas, your vault as a map#624
adibhanna merged 15 commits into
mainfrom
v2.30.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

The 2.30.0 release branch. The headline is Atlas, the vault drawn as a map; around it, a sweep of eight issues and the two fix sets from discussion #599.

Atlas: the vault as a map

The answer to years of "add the Obsidian graph" requests, built the ZenNotes way. Every note is a point sized by how linked it is, every wikilink (and markdown-style note link) a line, regions come from top-level folders, and the layout is computed once and frozen: new notes land beside what they link to and nothing else ever moves. v toggles between the flat map and a 3D sky, lenses 1..4 recolor it (structure, heat, orphans, bridges), f hint-jumps, Enter opens the note. Space g opens it, it is fully themeable (--z-atlas-bg, --z-atlas-region-1..8), on by default with a Settings toggle, and it never writes into the vault.

One filter across the Tasks views (#222, discussion #583)

The filter follows you across the list, the calendar, and the Kanban board, and now matches inline @key:value fields, so @project:alpha on the status board is a one-project Kanban. A filtered board keeps its columns still and preserves hand-made card order for what the filter hides.

The issue sweep

  • #614 Cmd+N creates a note in the current folder, rebindable.
  • #615 a disabled quick-capture hotkey stays disabled across restarts (no more GNOME shortcut dialog on launch).
  • #616 Live Preview off now means plain markdown: raw frontmatter, click-to-edit links.
  • #617 markdown links to notes with spaces collapse to their label like every other link.
  • #618 Workflows run on remote vaults in the desktop app, delegated to the server's journalled API (2.29+).
  • #621 the Assets row's m opens a real menu with sort options.
  • #622 [b and ]b take counts, wrapping around the tab ring.
  • #623 a Math size slider (50% to 200%) for KaTeX and Typst, portable as math_font_scale.

From discussion #599

Opening a note with the mouse hands you the keyboard; the in-progress [/] box is a real, clickable checkbox in the editor and reading view; and the [/] state survives Kanban drags and MCP/CLI toggles instead of collapsing to open. The Tasks view keeps its keys after any click inside it.

Full release notes land on the GitHub release when CI finishes the installers.

…asks view

Touching the list's filter box and pressing j walked the SIDEBAR cursor
instead of the task list, and after any click inside the view the 1/2/3
view switcher, /, and : went dead until the tab was reopened.

The pane's capture handlers claim focusedPanel 'editor' for every click
or focus inside a pane (#477), and the Tasks view's window keydown
handler bails unless the panel is 'tasks' (#412). Any interaction inside
the view therefore handed its keys to VimNav, whose global fallback
moved the sidebar cursor.

The view now re-claims 'tasks' on its root for both capture events.
Outer capture handlers run before inner ones, so the re-claim always
lands after the pane's claim and wins. TagView and Quick Notes share the
latent pattern and are left for their own change.
… board

Narrowing tasks to one project only worked in the list; the board and
the calendar always showed everything (#583, and the core of #222).

The header's filter box now mounts in all three sub-views and the query
survives switching between them. Matching gains inline @key:value fields
(they are stripped from display content, so they were unreachable) and
tags with the leading #, so @project:alpha on a status-grouped board is
a one-project Kanban. / focuses the box, :filter <text> sets it from the
ex line (:filter alone clears), Esc clears from anywhere, and the header
counts the slice (4 of 7).

On the board the filter narrows cards, never columns: the unfiltered
columns remain the source of truth for group-by discovery and card-order
persistence, and a drop resolves its index against the visible anchor
card before splicing into the full column, so a drag on a filtered board
cannot prune hidden cards out of a hand-made arrangement.

Deliberately not done: frontmatter tag inheritance for inline tasks
(it would change what tags a task carries in every mirrored parser) and
structured filter chips with AND/OR, which stay with #222.
Clicking a note in the sidebar or the notes panel opened the note but
left keyboard focus on the clicked row: typing went nowhere, and j/k
kept driving the sidebar. Every keyboard opener (quick open, Enter on a
sidebar row, gt, hint mode, Ctrl+O) already ends with
focusEditorNormalMode(); the mouse paths were the only note openers
that never called it.

The sidebar's single click, its double click (which re-focuses the row
after the first click's hand-off), and the note-list rows now do the
same hand-off. Cursor position was already remembered per note within a
session, so a click now lands exactly where you left off; the memory
was just invisible while the keyboard stayed in the sidebar.

Reported in discussion #599.
…every toggle

The half-filled [/] box was a drawing, not a control: clicking it did
nothing in the editor (an inert widget with no handler), and the
reading view rendered no control at all. Meanwhile the surfaces that
could change it were lossy: dragging an in-progress card between live
Kanban columns applied set-checked:false and collapsed the / to open,
and the MCP / zn CLI toggle flipped [/], [-], and [>] all back to open.

The marker widget and the preview's state span are now real checkboxes:
clicking checks the task off, the same [/] to [x] the toggle command
performs, with the selection left where it was. Forwarded and cancelled
markers stay deliberately inert; they are records of a decision, not
live work, and the toggle command already exempts them. No click-cycling
through states: the checkbox shape promises "click to complete", and
i / c / > and the right-click menu remain the explicit state setters.

The mutators now agree everywhere: toggleTaskAtIndex treats unchecking
a [/] as a no-op (in-progress already is not done), and the MCP toggle,
which the CLI reuses, follows the same open-and-done-flip / records-stay
rules instead of its own everything-to-open branch.

Reported in discussion #599.
People kept asking for the Obsidian graph, and the answer is deliberately
not that: Atlas draws the whole vault as a stable map instead of a physics
toy. Every note is a point sized by how linked it is, every wikilink a
line, regions come from top-level folders, and the layout is computed once,
frozen, and cached per vault, so the map stays a place you know. New notes
land beside what they link to; nothing else ever moves.

One view, two projections behind the v key: the flat 2D map and a 3D sky
you fly through (wheel travels toward the cursor, not just scale). Lenses
1..4 recolor it to answer questions: structure, heat (recent edits glow),
orphans, and bridges (betweenness, computed lazily). Hint jump, filter,
quiet/all/off links, region flights with [ ], Enter opens the note.

Keyboard-first, properly: Atlas is a first-class panel in vim navigation.
VimNav routes sidebar keys whenever no editor holds DOM focus, so a canvas
view must claim focusedPanel and be deferred to the way Tasks and Tags are,
leader passthrough included, or every keypress lands in the sidebar. Space
g opens it; Ctrl+W walks in and out. With vim off, arrows, Enter, Escape,
and the toolbar chips carry everything, per the house rule.

Theming is token-driven: the canvas reads the live CSS variables and
re-reads them when the theme flips. Custom themes can restyle the map with
--z-atlas-bg and --z-atlas-region-1 through --z-atlas-region-8.

Obsidian vaults get parity without any contract change: markdown-style
note links become edges too, scanned lazily inside the view through the
existing read-only bridge and cached per note mtime, so NoteMeta, the main
process, and the Go server are untouched and nothing is ever written into
the vault.

Settings: Atlas ships on by default with a toggle under Settings, stored
as atlas_enabled in config.toml. Deliberately left for follow-ups: link
clustering for regions, orbit rings, trace, replay, and the suggestion
write-back from the design doc (docs/ideas/atlas.md).

How to test locally: npm run build, launch, click Atlas in the sidebar or
press Space g. f then two letters jumps, v toggles map and sky, 2 shows
heat, drag pans level in 2D and orbits in 3D.
The command palette has had New Note in Current Folder since #403, but it
was never a bindable action, which is exactly where #614 landed: the
keybinding settings had nothing to attach a key to. It is now a first-class
shortcut, global.newNoteHere, default Cmd+N (previously unbound),
rebindable under Settings, and shown next to the palette entry.

One store action backs both the palette command and the shortcut so the
two can never drift. Semantics are unchanged from #403: the active note's
folder wins, the browsed folder is the fallback when no note is open, and
Trash never receives a note.

How to test locally: open any note inside a folder, press Cmd+N, and the
new note appears in that folder with the title focused. With no note open,
browse a folder and Cmd+N creates there.
[text](My Note.md) navigated fine but rendered half-collapsed: the label
lost its brackets while the raw (My Note.md) trailed beside it. CommonMark
refuses an unescaped space in a link destination, so the parser ends the
Link node at the closing bracket and the target hangs off the paragraph as
plain text; the live preview hid the link syntax it could see and left the
rest. Meanwhile the click and gd path reads targets with its own
space-tolerant scan, which is why the link worked while looking broken.

Rendering now matches navigation: a complete Link immediately followed by
a balanced (target) the parser rejected is treated as one link. The tail
hides with the brackets, and the cursor anywhere in the full span reveals
the whole thing as source, exactly like an ordinary markdown link. The
unterminated-target guard from #471 is untouched: while the closing paren
has not been typed yet, everything stays visible.

How to test locally: create My Note.md, then in another note write
[text](My Note.md) beside [text](Note.md). Both render as just the label;
cursor into either reveals the source; gd or click opens the right note.
Two behaviors ignored the Live Preview toggle. Frontmatter always rendered
as the compact properties card, so someone who wants raw markdown had to
break the fence with a deleted dash just to see their own text. And a plain
click inside [label](url) followed the link even though nothing was
rendered, so clicking into the URL to edit it opened the browser instead.

Both were rendering features applied outside the rendering gate. The
properties card (frontmatterStyle) now loads with the Live Preview bundle;
the frontmatter tag completion stays always-on, since it assists editing
rather than presentation. The plain-click follow from #201 existed to make
RENDERED links clickable, so it now checks the setting too; with Live
Preview off a click places the cursor, and Cmd/Ctrl+click still follows,
matching editor convention. With Live Preview on, nothing changes: card,
collapsed links, and click-to-follow all behave exactly as before.

How to test locally: turn Live Preview off in Settings, open a note with
frontmatter and a markdown link. The frontmatter reads as raw --- lines,
the link shows its full [label](url) source, and clicking inside the url
edits it. Turn Live Preview back on and both render again.
Disabling the quick capture hotkey persists an empty string, the settings
screen faithfully shows Disabled after a restart, and yet startup went on
to register the default shortcut anyway: the registration site read
cfg.quickCaptureHotkey || DEFAULT, and an empty string is falsey. On
Wayland with Electron's GlobalShortcutsPortal that re-registration invokes
the desktop portal, so every launch popped GNOME's Add Keyboard Shortcuts
dialog over the app. On other platforms it silently bound a shortcut the
user had turned off.

loadConfig always returns a normalized string here, and empty is the
user's explicit choice, so startup now passes it through unchanged;
registerQuickCaptureHotkey('') is already a clean no-op. Fresh installs
still get the default from DEFAULT_CONFIG, and only corrupt non-string
values fall back during normalization.

Diagnosis, including the exact offending expression, came from the
reporter. How to test locally: Settings, disable the quick capture hotkey,
quit, relaunch: the shortcut stays unbound (and on GNOME Wayland no
shortcuts dialog appears at launch).
…618)

The missing half of #608. That change gave the Go server a journalled
workflow API with capability negotiation and taught the web client to use
it; the desktop app in a remote workspace stayed hard-locked read-only,
with a comment promising delegation later. This is the delegation.

RemoteServerClient gains the seven workflow calls, mirroring the web
bridge exactly, including the split that matters: apply prepares the run
on the client side (reading files through the server) and posts the
prepared payload for the server's transactional apply, so conflict checks,
rollback, and interrupted-run recovery all happen where the files live.
The six workflow IPC handlers now route remote workspaces through that
client. The renderer gate asks the connected server for its capabilities
instead of trusting the static desktop ones, so servers without the API
keep the read-only view and the update-your-server error, exactly the
standard #608 set.

Verified end to end: a desktop build connected to a live Go server through
the real connect dialog shows the writable Workflows view, lists a
workflow stored on the server, opens it on the canvas, and dry-runs it.
Typecheck, all JS suites (two new gating cases), go vet, and the server's
httpserver tests pass.

How to test locally: run the server with an auth token, connect the
desktop app to it, open Workflows: create, edit, run, and undo all work,
and the run journal lives on the server.
3]b now walks three tabs forward and 2[b two back, Neovim-style, with the
no-count behaviour untouched. The asymmetry being fixed: [b already
honoured counts because it shares gT's relative action, but ]b shared
gt's, whose {count} is vim's ABSOLUTE tab number, so 3]b jumped to tab 3
instead of moving three forward. ]b now has its own relative action;
{count}gt keeps its absolute jump, and both wrap around the open-tab ring
the way the existing modulo navigation always has.

The global fallback layer (bracket sequences pressed outside a focused
editor, #321) had no counts at all, so it gains a vim-style digit prefix:
digits are recorded without being consumed, spent by the next [b or ]b,
expire on their own, and a leading 0 never starts a count, matching vim.

How to test locally: open a few tabs, focus a note, press 3]b and 2[b and
watch the tab strip; then click into the sidebar and do the same from
outside the editor. Without a count both keys still move exactly one tab.
Every vim-navigable sidebar row advertises m for its context menu, and
Assets showed the hint too, but it was the one row with no menu wired:
Archive and Trash pass their folder menus, Assets passed nothing, so the
synthesized contextmenu event landed on a row with no handler and nothing
happened.

Rather than hide the hint, Assets gets a menu worth opening: Open Assets,
and sort by name, type, size, last modified, or times used. Picking the
active field flips its direction the way a list header does, the active
field carries an up or down hint, and the choices drive the existing
assetSortOrder portable preference, so they stay in sync with the Assets
view and config.toml. Right-click on the row gets the same menu.

How to test locally: focus the sidebar, move the cursor onto Assets, press
m: the menu opens; pick Sort by size twice and the Assets view flips
between smallest-first and largest-first.
Math had exactly one size: whatever the typesetter derived from the text
around it. Settings gains Math size, a 50 to 200 percent slider under the
Math renderer picker, scaling inline $…$ and block $$…$$ math in the
editor and the reading view alike.

One factor covers both engines: a single zoom rule on the KaTeX container
and the Typst SVG output, driven by a --z-math-scale variable the app sets
from the preference. zoom rather than transform, so the surrounding line
layout grows with the formula instead of letting it overlap neighbours.

The preference is portable: math_font_scale under [editor] in config.toml,
clamped on the way in, searchable in Settings, documented in the manual.

How to test locally: open a note with $E = mc^2$ and a $$…$$ block,
drag Settings > Editor > Math size to 150 percent, and watch both grow in
the editor and preview; config.toml picks up math_font_scale = 150.
Two features shipped this cycle without their in-app manual entries.
The workflows chapter still claimed desktop remote workspaces are
read-only, which #618 made untrue: they author and run workflows now,
provided the server is on 2.29 or newer, and only against older servers
does the old read-only behavior remain. And the [b / ]b rows never
mentioned that #622 taught both motions to take a count, so 3]b jumps
three buffers forward around the ring.

The website docs said all of this already; this brings the second of
the two mirrored surfaces back in line. Text only, no behavior change.
@adibhanna
adibhanna merged commit d2f6e25 into main Aug 19, 2026
13 checks passed
@adibhanna
adibhanna deleted the v2.30.0 branch August 19, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant